• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

b1f6c1c4 / ProfessionalAccounting / 329

19 Sep 2025 05:22AM UTC coverage: 47.301% (-0.9%) from 48.177%
329

push

appveyor

b1f6c1c4
support WeeklyItem in $cf$

4951 of 10467 relevant lines covered (47.3%)

106.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.06
/AccountingServer.Shell/AccountingShell.cs
1
/* Copyright (C) 2020-2025 b1f6c1c4
2
 *
3
 * This file is part of ProfessionalAccounting.
4
 *
5
 * ProfessionalAccounting is free software: you can redistribute it and/or
6
 * modify it under the terms of the GNU Affero General Public License as
7
 * published by the Free Software Foundation, version 3.
8
 *
9
 * ProfessionalAccounting is distributed in the hope that it will be useful, but
10
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
12
 * for more details.
13
 *
14
 * You should have received a copy of the GNU Affero General Public License
15
 * along with ProfessionalAccounting.  If not, see
16
 * <https://www.gnu.org/licenses/>.
17
 */
18

19
using System;
20
using System.Collections.Generic;
21
using System.Linq;
22
using System.Security;
23
using AccountingServer.BLL;
24
using AccountingServer.BLL.Util;
25
using AccountingServer.Entities;
26
using AccountingServer.Entities.Util;
27
using AccountingServer.Shell.Subtotal;
28
using AccountingServer.Shell.Util;
29
using static AccountingServer.BLL.Parsing.FacadeF;
30
using static AccountingServer.BLL.Parsing.Synthesizer;
31

32
namespace AccountingServer.Shell;
33

34
/// <summary>
35
///     基本表达式解释器
36
/// </summary>
37
internal class AccountingShell : IShellComponent
38
{
39
    /// <inheritdoc />
40
    public IAsyncEnumerable<string> Execute(string expr, Context ctx, string term)
41
        => Parse(expr, ctx.Client)(ctx);
4✔
42

43
    /// <inheritdoc />
44
    public bool IsExecutable(string expr) => true;
4✔
45

46
    /// <summary>
47
    ///     解析表达式
48
    /// </summary>
49
    /// <param name="expr">表达式</param>
50
    /// <param name="client">客户端</param>
51
    /// <returns>解析结果</returns>
52
    private Func<Context, IAsyncEnumerable<string>> Parse(string expr, Client client)
53
    {
4✔
54
        var type = ExprType.None;
4✔
55
        ISubtotalStringify visitor;
56

57
        var explain = ParsingF.Token(ref expr, false, static t => t == "explain") != null;
4✔
58

59
        if (ParsingF.Token(ref expr, false, static t => t == "unsafe") != null)
4✔
60
            type |= ExprType.Unsafe;
2✔
61

62
        if (ParsingF.Token(ref expr, false, static t => t == "json") != null)
4✔
63
        {
×
64
            type |= ExprType.GroupedQueries;
×
65
            visitor = new JsonSubtotal();
×
66
        }
×
67
        else if (ParsingF.Token(ref expr, false, static t => t == "Rps") != null)
4✔
68
        {
×
69
            type |= ExprType.GroupedQueries;
×
70
            visitor = new PreciseSubtotalPre();
×
71
        }
×
72
        else if (ParsingF.Token(ref expr, false, static t => t == "rps") != null)
4✔
73
        {
×
74
            type |= ExprType.GroupedQueries;
×
75
            visitor = new PreciseSubtotalPre(false);
×
76
        }
×
77
        else if (ParsingF.Token(ref expr, false, static t => t == "raw") != null)
4✔
78
        {
×
79
            type |= ExprType.GroupedQueries | ExprType.DetailQuery;
×
80
            visitor = new RawSubtotal(0);
×
81
        }
×
82
        else if (ParsingF.Token(ref expr, false, static t => t == "sraw") != null)
4✔
83
        {
1✔
84
            type |= ExprType.GroupedQueries | ExprType.DetailRQuery;
1✔
85
            visitor = new RawSubtotal(0);
1✔
86
        }
1✔
87
        else if (ParsingF.Token(ref expr, false, static t => t == "fancy") != null)
3✔
88
        {
1✔
89
            type |= ExprType.FancyQuery;
1✔
90
            visitor = null;
1✔
91
        }
1✔
92
        else if (ParsingF.Token(ref expr, false, static t => t.StartsWith("raw", StringComparison.OrdinalIgnoreCase)) is var t && t != null)
2✔
93
        {
×
94
            type |= ExprType.GroupedQueries;
×
95
            visitor = new RawSubtotal(int.Parse(t[4..]))
×
96
                {
97
                    Ratio = t[3] switch
×
98
                        {
99
                            '+' => +1.0,
×
100
                            '-' => -1.0,
×
101
                            _ => throw new ApplicationException("Unknown raw ratio specifier."),
×
102
                        },
103
                    Inject = ParsingF.Quoted(ref expr, '@'),
104
                };
105
        }
×
106
        else
107
        {
2✔
108
            type |= ExprType.GroupedQueries | ExprType.VoucherQuery;
2✔
109
            visitor = new RichSubtotalPre(ParsingF.Token(ref expr, false, static t => t == "force") != null);
2✔
110
        }
2✔
111

112
        (visitor as IClientDependable).Assign(client);
4✔
113

114
        if (type.HasFlag(ExprType.GroupedQuery))
4✔
115
            try
116
            {
3✔
117
                return TryGroupedQuery(expr, visitor, client, explain);
3✔
118
            }
119
            catch (Exception)
3✔
120
            {
3✔
121
                // ignored
122
            }
3✔
123

124
        if (type.HasFlag(ExprType.VoucherGroupedQuery))
4✔
125
            try
126
            {
3✔
127
                return TryVoucherGroupedQuery(expr, visitor, client, explain);
3✔
128
            }
129
            catch (Exception)
3✔
130
            {
3✔
131
                // ignored
132
            }
3✔
133

134
        return (type & ExprType.NonGroupedQueries) switch
×
135
            {
136
                ExprType.VoucherQuery => TryVoucherQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
2✔
137
                ExprType.DetailQuery => TryDetailQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
×
138
                ExprType.DetailRQuery => TryDetailRQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
1✔
139
                ExprType.FancyQuery => TryFancyQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
1✔
140
                _ => throw new InvalidOperationException("表达式无效"),
×
141
            };
142
    }
3✔
143

144
    private async IAsyncEnumerable<string> Present(params string[] ss)
145
    {
×
146
        foreach (var s in ss)
×
147
            yield return $"{s}\n";
×
148
    }
×
149

150
    /// <summary>
151
    ///     按记账凭证检索式解析
152
    /// </summary>
153
    /// <param name="expr">表达式</param>
154
    /// <param name="safe">仅限强检索式</param>
155
    /// <param name="client">客户端</param>
156
    /// <returns>执行结果</returns>
157
    private Func<Context, IAsyncEnumerable<string>> TryVoucherQuery(string expr, bool safe, Client client, bool explain)
158
    {
2✔
159
        var res = ParsingF.VoucherQuery(ref expr, client);
2✔
160
        ParsingF.Eof(expr);
2✔
161
        if (res.IsDangerous() && safe && !explain)
1✔
162
            throw new SecurityException("检测到弱检索式");
×
163

164
        return ctx => explain
1✔
165
            ? Present("IQueryCompounded<IVoucherQueryAtom>", Synth(res), Synth(ctx.Identity.Refine(res, true)))
166
            : PresentVoucherQuery(ctx.Identity.Refine(res), ctx);
167
    }
1✔
168

169
    /// <summary>
170
    ///     按分类汇总检索式解析
171
    /// </summary>
172
    /// <param name="expr">表达式</param>
173
    /// <param name="trav">呈现器</param>
174
    /// <param name="client">客户端</param>
175
    /// <returns>执行结果</returns>
176
    private Func<Context, IAsyncEnumerable<string>> TryGroupedQuery(string expr, ISubtotalStringify trav, Client client,
177
            bool explain)
178
    {
3✔
179
        var res = ParsingF.GroupedQuery(ref expr, client);
3✔
180
        ParsingF.Eof(expr);
×
181
        return ctx => explain
×
182
            ? Present("IGroupedQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
183
            : PresentSubtotal(ctx.Identity.Refine(res), trav, ctx);
184
    }
×
185

186
    /// <summary>
187
    ///     按记账凭证分类汇总检索式解析
188
    /// </summary>
189
    /// <param name="expr">表达式</param>
190
    /// <param name="trav">呈现器</param>
191
    /// <param name="client">客户端</param>
192
    /// <returns>执行结果</returns>
193
    private Func<Context, IAsyncEnumerable<string>> TryVoucherGroupedQuery(string expr, ISubtotalStringify trav,
194
        Client client, bool explain)
195
    {
3✔
196
        var res = ParsingF.VoucherGroupedQuery(ref expr, client);
3✔
197
        ParsingF.Eof(expr);
×
198
        return ctx => explain
×
199
            ? Present("IVoucherGroupedQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
200
            : PresentSubtotal(ctx.Identity.Refine(res), trav, ctx);
201
    }
×
202

203
    /// <summary>
204
    ///     执行记账凭证检索式并呈现记账凭证
205
    /// </summary>
206
    /// <param name="query">记账凭证检索式</param>
207
    /// <param name="ctx">客户端上下文</param>
208
    /// <returns>记账凭证表达式</returns>
209
    private IAsyncEnumerable<string> PresentVoucherQuery(IQueryCompounded<IVoucherQueryAtom> query, Context ctx)
210
        => ctx.Serializer.PresentVouchers(ctx.Accountant.SelectVouchersAsync(query)
1✔
211
                .Select(ctx.Identity.RedactDetails));
212

213
    /// <summary>
214
    ///     按细目检索式解析
215
    /// </summary>
216
    /// <param name="expr">表达式</param>
217
    /// <param name="safe">仅限强检索式</param>
218
    /// <param name="client">客户端</param>
219
    /// <returns>执行结果</returns>
220
    private Func<Context, IAsyncEnumerable<string>> TryDetailQuery(string expr, bool safe, Client client, bool explain)
221
    {
×
222
        var res = ParsingF.DetailQuery(ref expr, client);
×
223
        ParsingF.Eof(expr);
×
224
        if (res.IsDangerous() && safe && !explain)
×
225
            throw new SecurityException("检测到弱检索式");
×
226

227
        return ctx => explain
×
228
            ? Present("IVoucherDetailQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
229
            : PresentDetailQuery(ctx.Identity.Refine(res), ctx);
230
    }
×
231

232
    /// <summary>
233
    ///     执行细目检索式并呈现结果
234
    /// </summary>
235
    /// <param name="query">细目检索式</param>
236
    /// <param name="ctx">客户端上下文</param>
237
    /// <returns>执行结果</returns>
238
    private IAsyncEnumerable<string> PresentDetailQuery(IVoucherDetailQuery query, Context ctx)
239
        => ctx.Serializer.PresentVoucherDetails(ctx.Accountant.SelectVoucherDetailsAsync(query));
×
240

241
    /// <summary>
242
    ///     按带记账凭证的细目检索式解析
243
    /// </summary>
244
    /// <param name="expr">表达式</param>
245
    /// <param name="safe">仅限强检索式</param>
246
    /// <param name="client">客户端</param>
247
    /// <returns>执行结果</returns>
248
    private Func<Context, IAsyncEnumerable<string>> TryDetailRQuery(string expr, bool safe, Client client, bool explain)
249
    {
1✔
250
        var res = ParsingF.DetailQuery(ref expr, client);
1✔
251
        ParsingF.Eof(expr);
1✔
252
        if (res.IsDangerous() && safe && !explain)
1✔
253
            throw new SecurityException("检测到弱检索式");
×
254

255
        return ctx => explain
1✔
256
            ? Present("IVoucherDetailQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
257
            : PresentDetailRQuery(ctx.Identity.Refine(res), ctx);
258
    }
1✔
259

260
    /// <summary>
261
    ///     执行带记账凭证的细目检索式并呈现结果
262
    /// </summary>
263
    /// <param name="query">细目检索式</param>
264
    /// <param name="ctx">客户端上下文</param>
265
    /// <returns>执行结果</returns>
266
    private IAsyncEnumerable<string> PresentDetailRQuery(IVoucherDetailQuery query, Context ctx)
267
        => ctx.Serializer.PresentVoucherDetails(
1✔
268
            ctx.Accountant.SelectVouchersAsync(query?.VoucherQuery).SelectMany(
269
                v => v.Details.Where(d => d.IsMatch(query?.ActualDetailFilter()))
4✔
270
                    .Select(d => new VoucherDetailR(v, d)).ToAsyncEnumerable()));
2✔
271

272
    /// <summary>
273
    ///     按记账凭证检索式解析,但仅保留部分细目
274
    /// </summary>
275
    /// <param name="expr">表达式</param>
276
    /// <param name="safe">仅限强检索式</param>
277
    /// <param name="client">客户端</param>
278
    /// <returns>执行结果</returns>
279
    private Func<Context, IAsyncEnumerable<string>> TryFancyQuery(string expr, bool safe, Client client, bool explain)
280
    {
1✔
281
        var res = ParsingF.DetailQuery(ref expr, client);
1✔
282
        ParsingF.Eof(expr);
1✔
283
        if (res.IsDangerous() && safe && !explain)
1✔
284
            throw new SecurityException("检测到弱检索式");
×
285

286
        return ctx => explain
1✔
287
            ? Present("IVoucherDetailQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
288
            : PresentFancyQuery(ctx.Identity.Refine(res), ctx);
289
    }
1✔
290

291
    /// <summary>
292
    ///     执行记账凭证检索式,保留部分细目并呈现结果
293
    /// </summary>
294
    /// <param name="query">带记账凭证的细目检索式</param>
295
    /// <param name="ctx">客户端上下文</param>
296
    /// <returns>执行结果</returns>
297
    private IAsyncEnumerable<string> PresentFancyQuery(IVoucherDetailQuery query, Context ctx)
298
        => ctx.Serializer.PresentVouchers(
1✔
299
            ctx.Accountant.SelectVouchersAsync(query?.VoucherQuery).Select(v =>
300
                {
1✔
301
                    v.Details.RemoveAll(d => !d.IsMatch(query?.ActualDetailFilter()));
4✔
302
                    return v;
1✔
303
                }));
1✔
304

305
    /// <summary>
306
    ///     执行记账凭证分类汇总检索式并呈现结果
307
    /// </summary>
308
    /// <param name="query">记账凭证分类汇总检索式</param>
309
    /// <param name="trav">呈现器</param>
310
    /// <param name="ctx">客户端上下文</param>
311
    /// <returns>执行结果</returns>
312
    private async IAsyncEnumerable<string> PresentSubtotal(IVoucherGroupedQuery query, ISubtotalStringify trav,
313
        Context ctx)
314
    {
×
315
        var result = await ctx.Accountant.SelectVouchersGroupedAsync(query);
×
316
        await foreach (var s in trav.PresentSubtotal(result, query.Subtotal, ctx.Serializer))
×
317
            yield return s;
×
318
    }
×
319

320
    /// <summary>
321
    ///     执行分类汇总检索式并呈现结果
322
    /// </summary>
323
    /// <param name="query">分类汇总检索式</param>
324
    /// <param name="trav">呈现器</param>
325
    /// <param name="ctx">客户端上下文</param>
326
    /// <returns>执行结果</returns>
327
    private async IAsyncEnumerable<string> PresentSubtotal(IGroupedQuery query, ISubtotalStringify trav,
328
        Context ctx)
329
    {
×
330
        var result = await ctx.Accountant.SelectVoucherDetailsGroupedAsync(query);
×
331
        await foreach (var s in trav.PresentSubtotal(result, query.Subtotal, ctx.Serializer))
×
332
            yield return s;
×
333
    }
×
334

335
    [Flags]
336
    private enum ExprType
337
    {
338
        None = 0b00000000,
339
        GroupedQueries = 0b00000011,
340
        GroupedQuery = 0b00000001,
341
        VoucherGroupedQuery = 0b00000010,
342
        NonGroupedQueries = 0b00111100,
343
        DetailQuery = 0b00000100,
344
        DetailRQuery = 0b00001000,
345
        VoucherQuery = 0b00010000,
346
        FancyQuery = 0b00100000,
347
        Unsafe = 0b10000000,
348
    }
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc