| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- <?php
- // php tests/device_operation_test.php;依赖已执行的设备运营迁移,所有测试数据事务回滚。
- require dirname(__DIR__) . '/vendor/autoload.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- \Webman\ThinkOrm\ThinkOrm::start(null);
- date_default_timezone_set(config('app.default_timezone', 'Asia/Shanghai'));
- use plugin\saiadmin\app\logic\operation\DeviceOperationLogic;
- use plugin\saiadmin\app\service\DeviceOperationMetrics;
- use plugin\saiadmin\app\service\DeviceOperationShares;
- use plugin\saiadmin\exception\ApiException;
- use support\think\Db;
- $checks = 0;
- function check(bool $condition, string $message): void
- {
- global $checks;
- ++$checks;
- if (!$condition) {
- throw new RuntimeException($message);
- }
- }
- function rejects(callable $call, string $message): void
- {
- try {
- $call();
- } catch (ApiException $e) {
- check(true, $message);
- return;
- }
- check(false, $message);
- }
- function listing(DeviceOperationLogic $logic, array $filters = []): array
- {
- return $logic->indexList(array_merge(['scope' => 'mine', 'category' => 'vehicle', 'limit' => 100], $filters));
- }
- $admin = new DeviceOperationLogic();
- $admin->init(['id' => 1]);
- $subId = (int) Db::table('sa_system_user')->where('id', '<>', 1)->whereNull('delete_time')->value('id');
- if (!$subId) {
- throw new RuntimeException('测试需要至少一个普通账号');
- }
- $sub = new DeviceOperationLogic();
- $sub->init(['id' => $subId]);
- $tag = 'TEST-OP-' . bin2hex(random_bytes(5));
- $now = new DateTimeImmutable();
- $today = $now->format('Y-m-d 00:00:00');
- $yesterday = $now->modify('-1 day')->format('Y-m-d 12:00:00');
- $started = $now->modify('-2 days')->format('Y-m-d 00:00:00');
- $countBefore = (int) Db::table('sa_device')->count();
- Db::startTrans();
- try {
- $createDevice = function (int $manager, string $suffix, int $type = 107) use ($tag, $started) {
- return (int) Db::table('sa_device')->insertGetId([
- 'goods_type' => $type, 'goods_sn' => $tag . '-' . $suffix,
- 'device_code' => $tag . '-' . $suffix, 'manage_agent_id' => $manager,
- 'belong_agent_id' => 1, 'create_time' => $started,
- ]);
- };
- $id = $createDevice(1, 'OWN');
- $other = $createDevice($subId, 'SUB');
- $deleted = $createDevice(1, 'DELETED');
- Db::table('sa_device')->where('id', $deleted)->update(['delete_time' => $today]);
- $merchantId = (int) Db::table('sa_merchant')->whereNull('delete_time')->value('id');
- Db::table('sa_merchant_device_binding')->insert([
- 'device_id' => $id, 'device_sn' => $tag . '-OWN', 'depend_type' => 107,
- 'merchant_id' => $merchantId, 'operator_id' => 1, 'bind_time' => $started,
- 'create_time' => $started, 'update_time' => $started,
- ]);
- $sequence = 0;
- $order = function (float $paid, float $refund, int $status, string $end, array $extra = []) use (&$sequence, $tag, $id, $started) {
- Db::table('sa_rental_order')->insert(array_merge([
- 'order_sn' => $tag . '-' . ++$sequence, 'device_id' => $id, 'device_type' => 107,
- 'manage_agent_id' => 1, 'status' => $status, 'paid_amount' => $paid,
- 'refund_amount' => $refund, 'rent_at' => $started, 'return_at' => $end,
- 'create_time' => $started, 'update_time' => $end,
- ], $extra));
- };
- $order(40, 10, 2, $yesterday); // 昨日净交易额 30
- $order(10, 0, 2, $today);
- $order(20, 0, 3, $today); // 同一天多单必须相加,不能覆盖
- $order(99, 0, 5, $today); // 扣款失败不得贡献收入/付费订单数
- $order(0, 0, 0, $today);
- $order(100, 0, 2, $today, ['delete_time' => $today]);
- $order(100, 0, 2, $today, ['manage_agent_id' => $subId]); // 不得泄露其他管理人订单
- $order(100, 0, 2, $today, ['rent_at' => $now->modify('+1 day')->format('Y-m-d H:i:s')]);
- Db::table('sa_device')->where('id', $id)->update(['cost_price' => 10]);
- Db::table('sa_merchant')->where('id', $merchantId)->update([
- 'agent_id' => 1, 'profit_method' => '比例分成', 'profit_rate' => 25,
- ]);
- $profile = ['profit_rates' => json_encode(['vehicle' => 80])];
- if (Db::table('sa_agent_profile')->where('agent_id', 1)->find()) {
- Db::table('sa_agent_profile')->where('agent_id', 1)->update($profile);
- } else {
- Db::table('sa_agent_profile')->insert($profile + [
- 'agent_id' => 1, 'product_codes' => '["vehicle"]', 'permissions' => '[]',
- 'create_time' => $today, 'update_time' => $today,
- ]);
- }
- $rows = array_column(listing($admin)['data'], null, 'id');
- $row = $rows[$id];
- check($row['total_amount'] === 60.0, '净交易额/退款/失败/删除/未来/权限过滤');
- check($row['today_amount'] === 30.0 && $row['yesterday_amount'] === 30.0, '每日金额应累加');
- check($row['total_orders'] === 5 && $row['today_orders'] === 4, '全部订单与日订单口径');
- check($row['paid_orders'] === 3 && $row['unit_price'] === 20.0, '客单价只除以有效付费订单');
- check($row['total_profit'] === 12.0 && $row['today_profit'] === 6.0, '商户有效分成盈利');
- check($row['merchant_rate'] === 20.0 && $row['manager_rate'] === 60.0, '从代理80%与商户相对25%自动计算20%/60%');
- check($row['profit_rate_value'] === 600.0, '盈利率沿用交易额/成本');
- check($row['daily_growth'] === 0.0, '日增长率');
- check($row['is_cost_recovery'] === true && $row['cost_recovery_date'] === substr($yesterday, 0, 10), '首次回本日期不得被后续订单覆盖');
- check($row['operated_days'] === 3 && $row['cost_recovery_days'] === 2, '投放与回本天数');
- check($row['today_performance'] === '优秀', '昨日/日均评级阈值');
- check(!isset($rows[$other]) && !isset($rows[$deleted]), '本人不包含下级及删除设备');
- check(!isset(array_column(listing($admin, ['scope' => 'subordinate'])['data'], null, 'id')[$id]), '下级不包含本人');
- check(isset(array_column(listing($admin, ['scope' => 'subordinate'])['data'], null, 'id')[$other]), '管理员可见下级');
- check(isset(array_column(listing($sub)['data'], null, 'id')[$other]), '普通账号本人范围');
- check(!isset(array_column(listing($sub)['data'], null, 'id')[$id]), '普通账号不可见其他管理人');
- foreach ([['scope' => 'all'], ['category' => 'unknown'], ['sort' => 'id DESC;DROP TABLE x'], ['order' => 'x']] as $bad) {
- rejects(fn () => listing($admin, $bad), '拒绝无效筛选/排序');
- }
- foreach (['ktv' => 100, 'bluetooth-cable' => 104, 'power-bank' => 101, 'password-cable' => 103, 'password-bluetooth-cable' => 105] as $category => $type) {
- $categoryId = $createDevice(1, $category, $type);
- check(isset(array_column(listing($admin, ['category' => $category])['data'], null, 'id')[$categoryId]), '设备分类查询');
- check(!isset(array_column(listing($admin)['data'], null, 'id')[$categoryId]), '分类隔离');
- }
- foreach (['total_amount', 'total_orders', 'unit_price', 'profit_rate_value'] as $sort) {
- $sorted = listing($admin, ['sort' => $sort, 'order' => 'desc']);
- $first = listing($admin, ['sort' => $sort, 'order' => 'desc', 'limit' => 1, 'page' => 1]);
- $second = listing($admin, ['sort' => $sort, 'order' => 'desc', 'limit' => 1, 'page' => 2]);
- check($first['data'][0]['id'] === $sorted['data'][0]['id'] && $second['data'][0]['id'] === $sorted['data'][1]['id'], '排序必须先于分页');
- check($first['total'] === $sorted['total'], '分页总数稳定');
- }
- Db::table('sa_merchant')->where('id', $merchantId)->update(['profit_rate' => 50]);
- $changed = array_column(listing($admin)['data'], null, 'id')[$id];
- check($changed['merchant_rate'] === 40.0 && $changed['total_profit'] === 24.0, '修改商户档案后运营统计自动变化');
- Db::table('sa_merchant')->where('id', $merchantId)->update(['profit_rate' => 25]);
- Db::table('sa_agent_profile')->where('agent_id', 1)->update(['profit_rates' => '{"vehicle":60}']);
- $changed = array_column(listing($admin)['data'], null, 'id')[$id];
- check($changed['merchant_rate'] === 15.0 && $changed['total_profit'] === 9.0, '修改代理档案后自动重新计算');
- Db::table('sa_agent_profile')->where('agent_id', 1)->update(['profit_rates' => '{}']);
- $unknown = array_column(listing($admin)['data'], null, 'id')[$id];
- check($unknown['total_profit'] === null && $unknown['is_cost_recovery'] === null, '未知参数不得假装为零/已回本');
- Db::table('sa_agent_profile')->where('agent_id', 1)->update($profile);
- Db::table('sa_device')->where('id', $id)->update(['cost_price' => 0]);
- $zero = array_column(listing($admin)['data'], null, 'id')[$id];
- check($zero['profit_rate_value'] === null && $zero['is_cost_recovery'] === true, '已确认零成本不能除零');
- Db::table('sa_device')->where('id', $id)->update(['manage_agent_id' => $subId]);
- $moved = array_column(listing($admin, ['scope' => 'subordinate'])['data'], null, 'id')[$id];
- check($moved['merchant_rate'] === 20.0 && $moved['cost_price'] === 0.0, '转移管理权不丢失台账成本和商户分成');
- check($moved['total_amount'] === 100.0, '转移后仍按可见订单管理人范围统计');
- $withoutYesterday = DeviceOperationMetrics::calculate(array_merge($row, ['yesterday_amount' => 0]), [], $now);
- check($withoutYesterday['daily_growth'] === null, '昨日无收入时增长率不得除零');
- // 新增产品和类型只改数据库字典,列表选项、分类与分成无需修改 PHP/Vue。
- $productType = Db::table('sa_system_dict_type')->where('code', 'device_product')->whereNull('delete_time')->value('id');
- $mappingType = Db::table('sa_system_dict_type')->where('code', 'device_type_product')->whereNull('delete_time')->value('id');
- Db::table('sa_system_dict_data')->insert([
- 'type_id' => $productType, 'code' => 'device_product', 'label' => '测试新产品', 'value' => 'test-product', 'status' => 1,
- ]);
- $mappingId = Db::table('sa_system_dict_data')->insertGetId([
- 'type_id' => $mappingType, 'code' => 'device_type_product', 'label' => 'test-product', 'value' => '65000', 'status' => 1,
- ]);
- $customId = $createDevice(1, 'CUSTOM', 65000);
- Db::table('sa_merchant_device_binding')->insert([
- 'device_id' => $customId, 'device_sn' => $tag . '-CUSTOM', 'depend_type' => 65000,
- 'merchant_id' => $merchantId, 'operator_id' => 1, 'bind_time' => $started,
- 'create_time' => $started, 'update_time' => $started,
- ]);
- Db::table('sa_agent_profile')->where('agent_id', 1)->update(['profit_rates' => '{"vehicle":80,"test-product":40}']);
- $order(20, 0, 2, $today, ['device_id' => $customId, 'device_type' => 65000]);
- $customList = listing($admin, ['category' => 'test-product']);
- check($customList['total'] === 1 && $customList['data'][0]['total_profit'] === 2.0, '数据库新增映射直接驱动分类与分成');
- check(in_array('test-product', array_column($customList['categories'], 'value'), true), '前端筛选选项来自数据库');
- Db::table('sa_system_dict_data')->where('id', $mappingId)->update(['status' => 2]);
- rejects(fn () => listing($admin, ['category' => 'test-product']), '禁用映射立即生效');
- Db::table('sa_system_dict_data')->where('id', $mappingId)->update(['status' => 1]);
- Db::table('sa_system_dict_data')->insert([
- 'type_id' => $mappingType, 'code' => 'device_type_product', 'label' => 'vehicle', 'value' => '65000', 'status' => 1,
- ]);
- rejects(fn () => listing($admin), '重复设备类型映射不得静默覆盖');
- } finally {
- Db::rollback();
- }
- check((int) Db::table('sa_device')->count() === $countBefore, '测试设备已回滚');
- check(Db::table('sa_rental_order')->whereLike('order_sn', $tag . '%')->count() === 0, '测试订单已回滚');
- // 多级代理扣除当前商户分支的直属下级比例,不能扣除整个代理树。
- $agents = [
- ['id' => 10, 'dept_id' => 10, 'dept_level' => '0,', 'profit_rates' => '{"vehicle":80}'],
- ['id' => 20, 'dept_id' => 20, 'dept_level' => '0,10,', 'profit_rates' => '{"vehicle":60}'],
- ['id' => 30, 'dept_id' => 30, 'dept_level' => '0,10,20,', 'profit_rates' => '{"vehicle":40}'],
- ];
- $shares = new DeviceOperationShares($agents);
- $device = ['merchant_id' => 1, 'merchant_agent_id' => 30, 'manage_agent_id' => 10,
- 'merchant_relative_rate' => 25, 'profit_method' => '比例分成'];
- check($shares->calculate($device, 'vehicle') === ['merchant_rate' => 10.0, 'manager_rate' => 20.0], '多层分成80-60与40*25%');
- check($shares->calculate(array_merge($device, ['manage_agent_id' => 30]), 'vehicle')['manager_rate'] === 30.0, '直属代理扣除商户有效比例');
- check($shares->calculate(array_merge($device, ['profit_method' => '不分成']), 'vehicle')['merchant_rate'] === 0.0, '不分成商户');
- check($shares->calculate($device, 'missing-product')['merchant_rate'] === null, '缺失产品分成不猜测');
- $agents[] = ['id' => 21, 'dept_id' => 20, 'dept_level' => '0,10,', 'profit_rates' => '{"vehicle":50}'];
- check((new DeviceOperationShares($agents))->calculate($device, 'vehicle')['manager_rate'] === null, '部门多人时不能猜分成代理');
- echo "PASS: $checks checks; test data rolled back.\n";
|